/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.atomojo.sparql;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.logging.Logger;

/**
 *
 * @author alex
 */
public class Parser {

   Logger log;
   public Parser() {
      this(Logger.getLogger(Parser.class.getName()));
   }
   public Parser(Logger log)
   {
      this.log = log;
   }
   
   public Query parseQuery(Reader input)
      throws IOException,ParseException
   {
      Lexer lexer = new Lexer(input);
      return parseQuery(lexer);
   }
   
   protected Query parseQuery(Lexer lexer)
      throws IOException,ParseException
   {
      Prologue prologue = parsePrologue(lexer);
      Lexer.Token current = lexer.peek();
      switch (current.getKeyword()) {
         case SELECT:
            return parseSelect(lexer,prologue);
         case CONSTRUCT:
            throw new ParseException("Construct expressions are not supported.",current.getLine(),current.getColumn());
         case DESCRIBE:
            throw new ParseException("Describe expressions are not supported.",current.getLine(),current.getColumn());
         case ASK:
            throw new ParseException("Ask expressions are not supported.",current.getLine(),current.getColumn());
         default:
            throw new ParseException("Unexpected keyword "+current.getKeyword(),current.getLine(),current.getColumn());
      }
   }
   
   protected Prologue parsePrologue(Lexer lexer)
      throws IOException,ParseException
   {
      Prologue prologue= new Prologue();
      Lexer.Token current = lexer.peek();
      boolean baseProcessed = false;
      boolean inPrologue = true;
      while (inPrologue) {
         switch (current.getType()) {
            case Identifier:
               switch (current.getKeyword()) {
                  case BASE:
                     if (baseProcessed) {
                        throw new ParseException("Unexpected addition base clause.",current.getLine(),current.getColumn());
                     }
                     parseBase(lexer,prologue);
                     baseProcessed = true;
                     current = lexer.peek();
                     break;
                  case PREFIX:
                     baseProcessed = true;
                     parsePrefix(lexer,prologue);
                     current = lexer.peek();
                     break;
                  default:
                     inPrologue = false;
               }
               break;
            default:
               throw new ParseException("Unexpected token "+current.getType()+" in prologue, "+current.getValue(),current.getLine(),current.getColumn());
         }
      }
      return prologue;
   }
   
   protected void parseBase(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      Lexer.Token base = lexer.next();
      if (base.getType()!=Lexer.Type.Identifier || base.getKeyword()!=Lexer.Keyword.BASE) {
         throw new ParseException("Unexpected token "+base.getValue(),base.getLine(),base.getColumn());
      }
      Lexer.Token iri = lexer.next();
      if (iri.getType()!=Lexer.Type.IRI) {
         throw new ParseException("Unexpected token "+iri.getValue(),iri.getLine(),iri.getColumn());
      }
      try {
         prologue.setBase(new URI(iri.getValue()));
      } catch (URISyntaxException ex) {
         throw new ParseException("Bad URI value: "+ex.getMessage(),iri.getLine(),iri.getColumn());
      }
   }
   
   protected void parsePrefix(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      Lexer.Token keyword = lexer.next();
      if (keyword.getType()!=Lexer.Type.Identifier || keyword.getKeyword()!=Lexer.Keyword.PREFIX) {
         throw new ParseException("Unexpected token "+keyword.getValue()+" "+keyword.getType(),keyword.getLine(),keyword.getColumn());
      }
      Lexer.Token next = lexer.next();
      if (next.getType()!=Lexer.Type.QName) {
         throw new ParseException("Expecting prefix but got '"+next.getValue()+"'",next.getLine(),next.getColumn());
      }
      String prefix = next.getValue();
      int colon = prefix.indexOf(':');
      if (prefix.substring(colon).length()>1) {
         throw new ParseException("Not expecting anything after colonin prefix: '"+next.getValue()+"'",next.getLine(),next.getColumn());
      }
      prefix = prefix.substring(0,colon);
      Lexer.Token iri = lexer.next();
      if (iri.getType()!=Lexer.Type.IRI) {
         throw new ParseException("Unexpected token "+iri.getValue(),iri.getLine(),iri.getColumn());
      }
      try {
         URI uri = prologue.resolve(iri.getValue());
         //log.info("Adding prefix "+prefix+" -> "+uri);
         Map<String,URI> prefixes = prologue.getPrefixes();
         if (prefixes.get(prefix)!=null) {
            throw new ParseException("Prefix "+prefix+" has already been defined.",next.getLine(),next.getColumn());
         }
         prefixes.put(prefix,uri);
      } catch (URISyntaxException ex) {
         throw new ParseException("Bad URI value: "+ex.getMessage(),iri.getLine(),iri.getColumn());
      }
      
   }
   
   protected SelectQuery parseSelect(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      SelectQuery query = new SelectQuery(prologue);
      Lexer.Token base = lexer.next();
      if (base.getType()!=Lexer.Type.Identifier || base.getKeyword()!=Lexer.Keyword.SELECT) {
         throw new ParseException("Unexpected token "+base.getValue(),base.getLine(),base.getColumn());
      }
      Lexer.Token next = lexer.peek();
      switch (next.getType()) {
         case Variable:
            while (next.getType()==Lexer.Type.Variable) {
               next = lexer.next();
               query.getReturnVariables().add(next.getValue());
               next = lexer.peek();
            }
            break;
         case Asterik:
            next = lexer.next();
            query.setReturnAll(true);
            next = lexer.peek();
            break;
      }
      
      if (next.getType()!=Lexer.Type.OpenCurlyBracket) {
         if (next.getType()!=Lexer.Type.Identifier) {
            throw new ParseException("Expecting 'WHERE' or 'FROM' but got token "+next.getValue(),next.getLine(),next.getColumn());
         }
         if (next.getKeyword()==Lexer.Keyword.FROM) {
            // consume
            next = lexer.next();
            // parse dataset
            next = lexer.next();
            boolean named = false;
            if (next.getType()==Lexer.Type.Identifier) {
               if (next.getKeyword()!=Lexer.Keyword.NAMED) {
                  throw new ParseException("Expecting 'NAMED' or IRI reference but got "+next.getValue(),next.getLine(),next.getColumn());
               }
               next = lexer.next();
               named = true;
            }
            if (next.getType()!=Lexer.Type.IRI) {
               throw new ParseException("Expecting IRI reference but got "+next.getValue(),next.getLine(),next.getColumn());
            }
            try {
               query.setDataSet(named,query.getPrologue().resolve(next.getValue()));
            } catch (URISyntaxException ex) {
               throw new ParseException("Bad IRI value: "+ex.getMessage(),next.getLine(),next.getColumn());
            }
         }
      }
      parseWhereClause(lexer,query);
      query.resolve();
      return query;
   }
   
   protected void parseWhereClause(Lexer lexer,Query query)
      throws IOException,ParseException
   {
      Lexer.Token next = lexer.next();
      if (next.getType()==Lexer.Type.Identifier) {
         if (next.getKeyword()!=Lexer.Keyword.WHERE) {
            throw new ParseException("Expecting 'WHERE' but got "+next.getValue(),next.getLine(),next.getColumn());
         }
         next = lexer.next();
      }
      if (next.getType()!=Lexer.Type.OpenCurlyBracket) {
         throw new ParseException("Expecting '{' but got "+next.getValue(),next.getLine(),next.getColumn());
      }
      WhereClause clause = new WhereClause();
      query.setWhereClause(clause);
      // parse where clause
      next = lexer.peek();
      boolean started = false;
      while (next.getType()!=Lexer.Type.EOF && next.getType()!=Lexer.Type.CloseCurlyBracket) {
         // start of triple;
         Ref subject = parseRef(lexer,query.getPrologue());
         if (subject==null) {
            next = lexer.peek();
            if (next.getKeyword()==Lexer.Keyword.FILTER) {
               parseFilter(lexer,clause,query.getPrologue());
               next = lexer.peek();
               continue;
            } else {
               throw new ParseException("Unexpected token "+next.getValue()+" "+next.getType(),next.getLine(),next.getColumn());
            }
         }
         //log.info("Subject: "+subject);
         started = true;
         boolean endOfList = false;
         while (!endOfList) {
            Ref predicate = parseRef(lexer,query.getPrologue());
            if (predicate==null) {
               next = lexer.peek();
               switch (next.getType()) {
                  case SemiColon:
                     // consume
                     lexer.next();
                     break;
                  case Period:
                     // consume
                     lexer.next();
                     endOfList = true;
                     break;
                  case CloseCurlyBracket:
                  case EOF:
                     endOfList = true;
                     break;
                  default:
                     throw new ParseException("Unexpected token "+next.getValue()+" in predicate list",next.getLine(),next.getColumn());
               }
            } else {
               //log.info("Predicate: "+predicate);
               // read the object list
               boolean endOfObjects = false;
               while (!endOfObjects) {
                  Ref object = parseRef(lexer,query.getPrologue());
                  if (object==null) {
                     next = lexer.peek();
                     switch (next.getType()) {
                        case Comma:
                           // consume
                           lexer.next();
                           break;
                        case SemiColon:
                           // consume
                           lexer.next();
                           endOfObjects = true;
                           break;
                        case Period:
                           // consume
                           lexer.next();
                           endOfList = true;
                           endOfObjects = true;
                           break;
                        case CloseCurlyBracket:
                           endOfList = true;
                           endOfObjects = true;
                           break;
                        case EOF:
                           endOfList = true;
                           endOfObjects = true;
                           break;
                        default:
                           throw new ParseException("Unexpected token "+next.getValue()+" in object list",next.getLine(),next.getColumn());
                     }
                  } else {
                     //log.info("Triple: "+subject+" "+predicate+" "+object);
                     clause.getClauses().add(new Triple(subject,predicate,object));
                  }
               }
            }
         }
         next = lexer.peek();
      }
      if (next.getType()==Lexer.Type.EOF) {
         throw new ParseException("Unexpected end of file before where clause close.",next.getLine(),next.getColumn());
      }
      if (started && clause.getClauses().isEmpty()) {
         throw new ParseException("Incomplete set of triples.",next.getLine(),next.getColumn());
      }
      // consume '}'
      lexer.next();
   }
   
   protected Ref parseRef(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      Ref ref = null;
      Lexer.Token next = lexer.peek();
      switch (next.getType()) {
         case Period:
         case SemiColon:
         case Comma:
         case CloseCurlyBracket:
         case Identifier:
            return null;
      }
      next = lexer.next();
      switch (next.getType()) {
         case Variable:
            ref = new VariableRef(next.getValue());
            break;
         case IRI:
            try {
               ref = new TermRef(prologue.resolve(next.getValue()));
            } catch (URISyntaxException ex) {
               throw new ParseException("Bad IRI value term: "+ex.getMessage(),next.getLine(),next.getColumn());
            }
            break;
         case Literal:
            ref = new LiteralRef(next.getValue());
            break;
         case QName:
            String qname = next.getValue();
            try {
               URI term = prologue.expand(qname);
               if (term==null) {
                  throw new ParseException("Cannot expand term reference: "+qname,next.getLine(),next.getColumn());
               }
               ref = new TermRef(term);
            } catch (URISyntaxException ex) {
               throw new ParseException("Bad term reference: "+ex.getMessage(),next.getLine(),next.getColumn());
            }
            break;
         case OpenParen:
            next = lexer.next();
            if (next.getType()!=Lexer.Type.CloseParen) {
               throw new ParseException("Expecting ')' but got "+next.getValue(),next.getLine(),next.getColumn());
            }
            ref = new NilRef();
            break;
         default:
            throw new ParseException("Unexpected token for term '"+next.getValue()+"'",next.getLine(),next.getColumn());
      }
      return ref;
      
   }
   
   protected void parseFilter(Lexer lexer,WhereClause clause,Prologue prologue)
      throws IOException,ParseException
   {
      Lexer.Token next = lexer.next();
      if (next.getType()!=Lexer.Type.Identifier || next.getKeyword()!=Lexer.Keyword.FILTER) {
         throw new ParseException("Expecting 'FILTER' but got "+next.getValue()+" "+next.getType(),next.getLine(),next.getColumn());
      }
      next = lexer.peek();
      switch (next.getType()) {
         case Identifier:
         case QName:
            throw new ParseException("Function calls are not supported: "+next.getValue(),next.getLine(),next.getColumn());
         case OpenParen:
            Expression e = parseBracketedExpression(lexer,prologue);
            if (e!=null) {
               clause.getFilterExpressions().add(e);
            }
            break;
      }
      
   }
   
   protected Expression parseBracketedExpression(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      Lexer.Token next = lexer.next();
      if (next.getType()!=Lexer.Type.OpenParen) {
         throw new ParseException("Expecting '(' but got "+next.getValue(),next.getLine(),next.getColumn());
      }
      
      Expression exp = parseOrExpression(lexer,prologue);
      next = lexer.next();
      if (next.getType()!=Lexer.Type.CloseParen) {
         throw new ParseException("Expecting ')' but got "+next.getValue(),next.getLine(),next.getColumn());
      }
      return exp;
   }
   protected Expression parseOrExpression(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      OperatorExpression orExpr = new OperatorExpression(OperatorExpression.Operator.Or);
      Expression exp = parseAndExpression(lexer,prologue);
      orExpr.getParts().add(exp);
      Lexer.Token op = lexer.peek();
      while (op.getType()==Lexer.Type.OR) {
         // consume
         lexer.next();
         exp = parseAndExpression(lexer,prologue);
         orExpr.getParts().add(exp);
         op = lexer.peek();
      }
      return orExpr.getParts().size()==1 ? exp : orExpr;
      
   }
   
   protected Expression parseAndExpression(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      OperatorExpression andExpr = new OperatorExpression(OperatorExpression.Operator.And);
      Expression exp = parseRelationalExpression(lexer,prologue);
      andExpr.getParts().add(exp);
      Lexer.Token op = lexer.peek();
      while (op.getType()==Lexer.Type.AND) {
         // consume
         lexer.next();
         exp = parseRelationalExpression(lexer,prologue);
         andExpr.getParts().add(exp);
         op = lexer.peek();
      }
      return andExpr.getParts().size()==1 ? exp : andExpr;
   }   
   protected Expression parseRelationalExpression(Lexer lexer,Prologue prologue)
      throws IOException,ParseException
   {
      
      Lexer.Token next = null;
      Ref leftSide = parseRef(lexer,prologue);
      if (leftSide==null) {
         next = lexer.peek();
         throw new ParseException("Missing left hand side of the expression.",next.getLine(),next.getColumn());
      }
      
      RelationalExpression.Operator operator;
      next = lexer.next();
      switch (next.getType()) {
         case EQUALS:
            operator = RelationalExpression.Operator.EQUALS;
            break;
         case NOT_EQUALS:
            operator = RelationalExpression.Operator.NOT_EQUALS;
            break;
         default:
            throw new ParseException("Unrecognized operator expression: "+next.getValue(),next.getLine(),next.getColumn());
      }
      
      Ref rightSide = parseRef(lexer,prologue);
      if (rightSide==null) {
         next = lexer.peek();
         throw new ParseException("Missing right hand side of the expression.",next.getLine(),next.getColumn());
      }
      return new RelationalExpression(leftSide,operator,rightSide);
      
   }
   
   public Logger getLogger() {
      return log;
   }
   
   public static void main(String [] args) {
      Parser parser = new Parser();
      for (String arg : args) {
         try {
            File file = new File(arg);
            FileReader r = new FileReader(file);
            Query q = parser.parseQuery(r);
            r.close();
            parser.getLogger().info("\n"+q.toString());
            if (q instanceof SelectQuery) {
               parser.getLogger().info("Return set: "+((SelectQuery)q).getReturnVariables());
            }
         } catch (Exception ex) {
            ex.printStackTrace();
         }
      }
   }
   
}
